import { diff } from 'json-diff';
import {
  cloneDeep,
  compact,
  difference,
  differenceBy,
  flow,
  isEmpty,
  sortBy,
  transform,
  uniqBy,
} from 'lodash-es';
import omitDeep from 'omit-deep-lodash';
import { deepRound } from './number';

const log = console.log.bind(null, '[utils-diff]');

export enum EDiffType {
  Modified = '~',
  Added = '+',
  Removed = '-',
  Unchanged = ' ',
}
export type AnyRecord = Record<any, any>;
export type IDiffContent<T extends AnyRecord = any> = Partial<T> | AnyRecord;
export type IDiffItem<T extends AnyRecord> = [EDiffType, IDiffContent<T>];
export interface ISourceItem<T> {
  oldItem?: T;
  newItem?: T;
}
export type ISourceDataTuple<T> = [oldItem: T, newItem: T];
export type IDiffItemWithSourceData<T extends AnyRecord> = [
  ...IDiffItem<T>,
  Partial<ISourceItem<T>>,
];

interface IDiffParams<T> {
  oldList: T[];
  newList: T[];
  diffKey: string;
  omitKeys: string[];
}

export function getShallowDiffList<T extends AnyRecord>(
  oldList: T[],
  newList: T[],
  diffKey: string,
): {
  diffList: IDiffItemWithSourceData<T>[];
  unDiffList: ISourceItem<T>[];
} {
  type IDiffListWidthSourceData = IDiffItemWithSourceData<T>[];

  const removed = differenceBy(oldList, newList, diffKey);
  const added = differenceBy(newList, oldList, diffKey);

  let oldListWillDiffed = differenceBy(oldList, removed, diffKey);
  let newListWillDiffed = differenceBy(newList, added, diffKey);

  oldListWillDiffed = sortBy(oldListWillDiffed, diffKey);
  newListWillDiffed = sortBy(newListWillDiffed, diffKey);
  newListWillDiffed = uniqBy(newListWillDiffed, diffKey);

  // log({ removed, added, oldListWillDiffed, newListWillDiffed });

  let diffListWithSource: IDiffListWidthSourceData = [];

  removed.forEach((item) => {
    diffListWithSource.push([
      EDiffType.Removed,
      item,
      { oldItem: item, newItem: undefined },
    ]);
  });
  added.forEach((item) => {
    diffListWithSource.push([
      EDiffType.Added,
      item,
      { oldItem: undefined, newItem: item },
    ]);
  });

  const unDiffList: ISourceItem<T>[] = [];

  const willDiffedListLen = oldListWillDiffed.length;
  for (let index = 0; index < willDiffedListLen; index++) {
    const sourceItem: ISourceItem<T> = {
      oldItem: oldListWillDiffed[index],
      newItem: oldListWillDiffed[index],
    };
    unDiffList.push(sourceItem);
  }

  return {
    diffList: diffListWithSource,
    unDiffList,
  };
}

export function getDiffList<T extends AnyRecord>(
  oldList: T[],
  newList: T[],
  diffKey: string,
  omitKeys: string[],
): IDiffItemWithSourceData<T>[] {
  type IDiffList = IDiffItem<T>[];
  type IDiffListWidthSourceData = IDiffItemWithSourceData<T>[];
  function diffPreHandle(data: T) {
    return flow(
      (data) => cloneDeep(data),
      // (data) => omitDeep(data, omitKeys),
      (data) => deepRound(data, 3),
    )(data);
  }
  const removed = differenceBy(oldList, newList, diffKey);
  const added = differenceBy(newList, oldList, diffKey);

  let oldListWillDiffed = differenceBy(oldList, removed, diffKey);
  let newListWillDiffed = differenceBy(newList, added, diffKey);

  oldListWillDiffed = sortBy(oldListWillDiffed, diffKey);
  newListWillDiffed = sortBy(newListWillDiffed, diffKey);
  newListWillDiffed = uniqBy(newListWillDiffed, diffKey);

  // log({ removed, added, oldListWillDiffed, newListWillDiffed });

  const willDiffedListLen = oldListWillDiffed.length;

  let diffListWidthSource: IDiffListWidthSourceData = [];
  for (let index = 0; index < willDiffedListLen; index++) {
    const oldItem = oldListWillDiffed[index];
    const newItem = newListWillDiffed[index];
    let diffList_sub: IDiffList = diff(
      [oldItem].map(diffPreHandle),
      [newItem].map(diffPreHandle),
      { sort: true, excludeKeys: omitKeys, keepUnchangedValues: true },
    );
    const sourceData_sub: ISourceItem<T> = {
      oldItem,
      newItem,
    };

    // log({ diffList_sub });

    if (isEmpty(diffList_sub)) {
      diffList_sub = [[EDiffType.Unchanged, oldItem]];
    }

    const diffListWidthSource_sub = [
      [...diffList_sub[0], sourceData_sub],
    ] as IDiffListWidthSourceData;

    // log({ diffListWidthSource_sub });

    diffListWidthSource.push(...diffListWidthSource_sub);
  }

  removed.forEach((item) => {
    diffListWidthSource.push([
      EDiffType.Removed,
      item,
      { oldItem: item, newItem: undefined },
    ]);
  });
  added.forEach((item) => {
    diffListWidthSource.push([
      EDiffType.Added,
      item,
      { oldItem: undefined, newItem: item },
    ]);
  });

  diffListWidthSource.reverse();

  return diffListWidthSource;
}

export function arrayDiffFix<T extends AnyRecord>(
  oldList: T[],
  newList: T[],
  omitKeys: string[],
) {
  const getDiffLink = (list: T[]) => {
    const listStringify: string[] = [];
    const listMap = new Map<string, T>();
    list.forEach((item) => {
      const itemStringify = JSON.stringify(diffPreHandle(item, omitKeys));
      listStringify.push(itemStringify);
      listMap.set(itemStringify, item);
    });
    return [listStringify, listMap] as const;
  };
  const [[oldListStringify, oldListMap], [newListStringify, newListMap]] = [
    oldList,
    newList,
  ].map(getDiffLink);
  const removed = difference(oldListStringify, newListStringify);
  const added = difference(newListStringify, oldListStringify);
  const oldListUnchanged = difference(oldListStringify, removed);
  const diffItems: IDiffItem<T>[] = [];
  removed.forEach((item) => {
    diffItems.push([EDiffType.Removed, oldListMap.get(item)!]);
  });
  added.forEach((item) => {
    diffItems.push([EDiffType.Added, newListMap.get(item)!]);
  });
  oldListUnchanged.forEach((item) => {
    diffItems.push([EDiffType.Unchanged, oldListMap.get(item)!]);
  });
  return diffItems;
}

export function diffPreHandle<T extends AnyRecord>(
  data: T,
  omitKeys: string[],
): Partial<T> {
  return flow(
    (data) => cloneDeep(data),
    (data) => omitDeep(data, omitKeys),
    (data) => deepRound(data, 3),
  )(data);
}

type FullDataItem<T> = Partial<ISourceDataTuple<T>>;

export function getFullData<T extends AnyRecord>(
  _oldList: T[] | undefined,
  _newList: T[] | undefined,
  diffList: IDiffItemWithSourceData<T>[],
): FullDataItem<T>[] {
  return diffList
    .map<FullDataItem<T>>((item) => {
      const sourceData = item[2];
      return [sourceData.oldItem, sourceData.newItem];
    })
    .map(compact) as any;
}

export function createRecord<T>(): T {
  return {} as T;
}

export function getSourceItem<T extends AnyRecord>(
  diffItem: IDiffItemWithSourceData<T>,
) {
  const sourceItem = diffItem[2];
  return sourceItem;
}

export function getSourceItemValue<T extends AnyRecord>(
  sourceItem: Partial<ISourceItem<T>>,
  key: string,
) {
  const value = (sourceItem.oldItem?.[key] ?? sourceItem.newItem?.[key])!;
  return value;
}

export function getSourceList<T extends AnyRecord>(
  diffList: IDiffItemWithSourceData<T>[],
): Partial<ISourceItem<T>>[] {
  return diffList.map((item) => {
    const sourceData = item[2];
    return sourceData;
  });
}

export function diffArr2Obj<T extends AnyRecord>(
  diffItem: IDiffItemWithSourceData<T>,
) {
  const [type, content, source] = diffItem;
  return { type, content, source };
}

export function isChangedDiffItem<T extends AnyRecord>(
  diffItem: IDiffItemWithSourceData<T>,
) {
  const isUnchanged = diffArr2Obj(diffItem).type === EDiffType.Unchanged;
  return !isUnchanged;
}

export const diffClassNameMap: Record<EDiffType, string> = {
  [EDiffType.Added]: 'color-green',
  [EDiffType.Removed]: 'color-red',
  [EDiffType.Modified]: 'color-yellow',
  [EDiffType.Unchanged]: '',
};

export const diffTextMap: Record<EDiffType, string> = {
  [EDiffType.Added]: 'add',
  [EDiffType.Removed]: 'delete',
  [EDiffType.Modified]: 'json_diff',
  [EDiffType.Unchanged]: 'unchange',
};

export const diffOptions = transform(
  diffTextMap,
  (result: { label: string; value: string }[], value, key) => {
    result.push({
      label: value,
      value: key,
    });
  },
  [],
);

export function getChangedSourceValueOfDiffItems<
  T extends AnyRecord,
  K extends keyof T,
>(
  diffItems: IDiffItem<T>[],
  source: { oldItems: T[]; newItems: T[] },
  key: K,
): T[K][] {
  const newItem = [...source.newItems];
  const oldItem = [...source.oldItems];

  function pushValueToResult(result: T[K][], items: T[], key: K) {
    const value = items.shift()?.[key];
    if (!value) return;
    result.push(value);
  }

  return transform(
    diffItems,
    (result, diff) => {
      const [type] = diff;

      switch (type) {
        case EDiffType.Added:
          pushValueToResult(result, newItem, key);
          break;
        case EDiffType.Removed:
          pushValueToResult(result, oldItem, key);
          break;
        case EDiffType.Modified:
          pushValueToResult(result, oldItem, key);
          pushValueToResult(result, newItem, key);
          break;
        case EDiffType.Unchanged:
          oldItem.shift();
          newItem.shift();
          break;
      }
    },
    [] as T[K][],
  );
}

export function getChangedSourceValueOfDiffItemsV2<
  T extends AnyRecord,
  K extends keyof T,
>(
  diffItems: IDiffItem<T>[],
  source: { oldItems: T[]; newItems: T[] },
  key: K,
): T[K][] {
  return transform(
    diffItems,
    (result, diff) => {
      const [type, content] = diff;

      switch (type) {
        case EDiffType.Added:
        case EDiffType.Removed:
        case EDiffType.Modified:
          result.push((content as T)[key]);
          break;
        case EDiffType.Unchanged:
          break;
      }
    },
    [] as T[K][],
  );
}
export function unKeepUnchangedValue<T extends AnyRecord>(
  diffItems: IDiffItem<T>[],
): IDiffItem<T>[] {
  return diffItems.map<IDiffItem<T>>((item) => {
    const [type, content] = item;
    if (type === EDiffType.Unchanged) {
      return [type] as unknown as IDiffItem<T>;
    }
    return [type, content];
  });
}
type MaybeDiffItem<T extends AnyRecord> = IDiffItem<T> | undefined;
export function isDiffItem<T extends AnyRecord>(
  item: MaybeDiffItem<T>,
): item is IDiffItem<T> {
  const type = item?.[0];
  if (!type) return false;
  return Object.values(EDiffType).some((diffType) => diffType === type);
}
export function getDiffContentLite<T extends AnyRecord, K extends keyof T>(
  diffContent: IDiffContent<T>,
  key: K,
): IDiffContent<T> {
  const { [key]: willLiteData, otherData } = diffContent;
  if (!Array.isArray(willLiteData)) return diffContent;
  return unKeepUnchangedValue(willLiteData as IDiffItem<T>[]);
}
